// ==UserScript==
// @name Freemissions
// @version 1.3
// @internal-version 3.2
// @description Take back your browser's JavaScript/ECMAScript. Culls intrusive JavaScript functions.
// @author <null>
// @match https://*/*
// @match http://*/*
// @include *
// @grant unsafeWindow
// @grant GM_getValue
// @grant GM_setValue
// @grant GM_registerMenuCommand
// @grant GM_addStyle
// @run-at document-start
// @run-at document-end
// @priority 9001
// @connect *
// @icon http://null.rf.gd/pages/resources/freemissions2.png
// @require https://raw.github.com/odyniec/MonkeyConfig/master/monkeyconfig.js
// ==/UserScript==

/*
 * NOTICE for those coming from 1.1.1 and earlier.
 * There is now a new settings menu, goto your script manager and enter it to see a selection of choices to enhance your Freemissions experience.
 */

/* 
 * Freemissions (3)
 * Your privacy in your control.
 *
 * Changelog:
 * pre-1.0.0:
 *  - Inital release
 * pre-2.0.0:
 *  - Inital release of v2
 *  - Complete rewrite
 *  - Added inital SafeRTC implementation
 * pre-3.0.0:
 *  - Inital release of v3
 *  - Partial rewrite of Freemissions
 * pre-3.0.1:
 *  - Added WebGL protection
 *  - Added SafeRTC (partial WebRTC protection, full will be released in the future)
 *  - (later build) Denying the prompt will return a FAKE list of media devices. (from 3 to 11 fake devices)
 *  - (not included in source) - Added base for AUTOMATED tracker detection
 *  - Known trackers will now automatically be protected by SafeRTC, SafeCanvas, etc...
 *  - Added possiblity of breaking reCaptcha v3 (for your benefit) (woops) (^1)
 * pre-3.0.2:
 *  - Added SafeDRM. (Allows you to block DRM access to sites.) (ovr::navigator.requestMediaKeyAccess:func)
 *  - Improved extreme mode values.
 *  - Improved log output layout.
 *  - SafeCanvas: The "2d" context is now automatically allowed (except on EXTREME/BLOCKED sites)
 *  - SafeCanvas: 2D protection has been marked as 'TO IMPROVE'
 *  - Added partial incognito mode protection (currently protects from FileSystem detection)
 * 1.0: (pre-3.0.2.1 - public release)
 *  - Flash Player fixes (also other Plugins, TODO: Automatic Flash Usage detection, skip that ANNOYING prompt)
 *  - Redesigned Plugin Faking (wow, it's a blank list!) (Somehow made my score go from 0.7 (One Stage Captcha) to 0.9 (Automatic))
 *  - Hid modified functions when toString() and toSource() is called.
 * 1.1: (pre-3.0.3)
 *  - Updated Permissions API spoofing (All sites should think that you automatically give permission to various features.)
 *  - Updated toString() and toSource() values.
 *  - Updated osCPU spoofing.
 *  - Hid some stored original functions that were left exposed.
 *  - 2D canvases will now report spoofed data on toDataURL() and toBlob()
 *  - WebGL and WebGL 2 have been protected from canvas fingerprinting.
 *  - Performance improvements. (Freemissions now loads on Promises if supported)
 *  - Added Platform spoofing.
 *  - Added Intl API spoofing. (Affects time zone only)
 *  - Freemissions no longer crashes and burns when some features (such as DRM) are unsupported.
 *  - Console output no longer shows '[Freemissions 3]'
 * 1.1.1: (pre-3.0.4)
 *  - Spotify will now report a seperate User Agent to allow it's DRM to work.
 *  - Updated DRM database.
 *  - Permissions API is now blocked for reCaptcha.
 *  - Fixed SafeDRM for chromium browsers.
 * (Sidenote: 1.1.1 seems to handle Spotify Web without an issue.)
 * 1.2: (pre-3.1)
 *  - Added config menu at the cost of an additional library.
 *  - Code cleanup.
 *  - Captcha mode is now changeable.
 *  - Better handling of unsupported features. (including SafeRTC)
 *  - Removed Audio API spoofing test.
 *  - Depreacted SAFE and BLOCKED_CONTENT operating modes.
 *  - Updated Plugin API spoofing. (Added MimeType support)
 *
 * ^1 - Not FULLY confirmed, tested on isolated devices with multiple browser profiles, IPs and Freemissions 3
 * 
 * reCaptcha v3 score guide:
 * >=0.9 Automatic PASS
 * >=0.7 One Stage Captcha
 * >=0.5 ????
 * >=0.3 Multi Stage Captcha (fading images)
 * >=0.1 Multi Stage Captcha (slow fading + voice captcha is blocked)
 */

function accquire_settings(obj) {
	if (typeof MonkeyConfig === "function") {
		return new MonkeyConfig(obj);
	}else {
		return {
			get: function(key) {
				return null;
			},
			set: function(key, value) {},
		};
	}
}

const cfg = accquire_settings({
	title: 'Freemissions v1.3 Settings',
	menuCommand: true,
	params: {
		captcha_mode: {
			label: 'ReCaptcha Mode behaviour',
			type: 'select',
			choices: [ 'Do Nothing', 'Isolated Variables', 'Partial Undefine', 'Full Undefine (beta)', 'virtPTCHA (early alpha)'],
			default: 'Partial Undefine',
		},
		canvas_behaviour: {
			label: 'Canvas Spoofing behaviour',
			type: 'select',
			choices: ['Spoof Initial Canvas (faster)', 'Spoof Every Call (slower)'],
			default: 'Spoof Every Call (slower)',
		},
		plugin_behaviour: {
			label: 'Plugin Spoofing behaviour',
			type: 'select',
			choices: ['Prefer Blank Lists', 'Prefer Fake Plugins', 'Do Not Spoof'],
			default: 'Prefer Blank Lists',
		},
		plugin_permission: {
			label: 'Plugin Access',
			type: 'select',
			choices: ['Automatically grant', 'Always Ask', 'Automatically deny'],
			default: 'Always Ask',
		},
		tracker_confuser: {
			label: 'Confuse trackers by spoofing multiple browser features. (alpha)',
			type: 'checkbox',
			default: false,
		},
		grant_webgl_auto: {
			label: 'Automatically grant WebGL contexts',
			type: 'checkbox',
			default: false,
		}
	},
});

function read_setting(key, fallback) {
	var read = cfg.get(key);
	if (read == null || read == undefined) {
		return fallback;
	}
	return read;
}

function __hasScriptToRC() {
	var item;
	var i = 0;
	var list = document.getElementsByTagName("script");
	for (; i < list.length; i++) {
		item = list[i];
		if (item == undefined || item == null) {
			continue;
		}else if (item.src.indexOf("www.gstatic.com/recaptcha/") > -1) {
			return true;
		}
	}
	return false;
}

function isRecaptchaPage() {
	return window.location.toString().indexOf("www.google.com/recaptcha/api2/anchor") > -1
				|| window.location.toString().indexOf("www.google.com/recaptcha/api2/bframe") > -1;
}

function hasKnownTrackers() {
	if (window.location.toString().indexOf("disqus.com/embed/comments/") > -1) {
		return true;
	}
	return false;
}

var OperatingMode = {
	STANDARD: 0,
	SAFE: 1, // Deprecated, planned removal: 1.3
	EXTREME: 2,
	BLOCKED_CONTENT: 3, // Deprecated, replaced by eval() protection and other features.
	/**
	  * Introduced: 3.0.2
	  * Used for a "pure" enviroment for Google ReCaptcha
	  */
	CAPTCHA_ENV: 4,
};

function getMode() {
	if (isRecaptchaPage()) {
		return OperatingMode.CAPTCHA_ENV;
	}else if (hasKnownTrackers()) {
		return OperatingMode.EXTREME;
	}else{
		return OperatingMode.STANDARD;
	}
}

const CHROME_VERSION = "81.0.4044.138";
const CHROME_USERAGENTS = [
	"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/$VERSION Safari/537.36",
	"Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/$VERSION Safari/537.36",
	"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/$VERSION Safari/537.36",
	"Mozilla/5.0 (Windows NT 5.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/$VERSION Safari/537.36",
];

function Random(n) {
	const shift = Math.ceil(Math.log2(n));
	const factor = n / Math.pow(2, shift);
	const mask = (1 << shift) - 1;

	let index = Infinity, buffer = [ ];

	function get() {
		index = 0;
		let random = Math.random() * Number.MAX_SAFE_INTEGER;
		for (let i = shift, j = 0; i < 32; i += shift, ++j) {
			buffer[j] = ((random & mask) * factor) << 0;
			random = random >> shift;
		}
	}

	return function() {
		if (index >= buffer.length) { get(); }
		return buffer[index++];
	};
}

function makeid(length) {
  	var text = "";
  	var possible = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
  	for (var i = 0; i < length; i++) {
    	text += possible.charAt(Math.floor(Math.random() * possible.length));
  	}
  	return text;
}

function __fakeGetters(object, options, getters) {
	if (!getters) { getters = options; options = { }; }
	const bind = options && options.bind;
	const old = options && options.old;
	Object.keys(getters).forEach(key => {
		const get = old && Object.getOwnPropertyDescriptor(object, key).get;
		Object.defineProperty(object, key, { get: bind ? getters[key].bind(bind, get) : get ? function() { return getters[key].call(this, get); } : getters[key], });
	});
	return object;
}

function Decrementor(source) {
	let current = source();
	function get() {
		if (!current) {
			current = source();
			return true;
		}
		--current;
		return false;
	}
	return get;
}

function rnd(a, b) {
    //calling rnd() with no arguments is identical to rnd(0, 100)
    a = a || 0;
    b = b || 100;

    if (typeof b === 'number' && typeof a === 'number') {
        //rnd(int min, int max) returns integer between min, max
        return (function (min, max) {
            if (min > max) {
                throw new RangeError('expected min <= max; got min = ' + min + ', max = ' + max);
            }
            return Math.floor(Math.random() * (max - min + 1)) + min;
        }(a, b));
    }

    if (Object.prototype.toString.call(a) === "[object Array]") {
        //returns a random element from array (a), even weighting
        return a[Math.floor(Math.random() * a.length)];
    }

    if (a && typeof a === 'object') {
        //returns a random key from the passed object; keys are weighted by the decimal probability in their value
        return (function (obj) {
            var rand = rnd(0, 100) / 100, min = 0, max = 0, key, return_val;

            for (key in obj) {
                if (obj.hasOwnProperty(key)) {
                    max = obj[key] + min;
                    return_val = key;
                    if (rand >= min && rand <= max) {
                        break;
                    }
                    min = min + obj[key];
                }
            }

            return return_val;
        }(a));
    }

    throw new TypeError('Invalid arguments passed to rnd. (' + (b ? a + ', ' + b : a) + ')');
}

const PLATFORMS = [
	"Android", "Linux", "FreeBSD",
	"FreeBSD i386", "FreeBSD amd64",
	"Linux x86_64", "Linux i686",
	"Windows", "Win16", "Win32", "PlayStation 4", "SunOS"
];

function randomLang() {
    return rnd(['AB', 'AF', 'AN', 'AR', 'AS', 'AZ', 'BE', 'BG', 'BN', 'BO', 'BR', 'BS', 'CA', 'CE', 'CO', 'CS',
                'CU', 'CY', 'DA', 'DE', 'EL', 'EN', 'EO', 'ES', 'ET', 'EU', 'FA', 'FI', 'FJ', 'FO', 'FR', 'FY',
                'GA', 'GD', 'GL', 'GV', 'HE', 'HI', 'HR', 'HT', 'HU', 'HY', 'ID', 'IS', 'IT', 'JA', 'JV', 'KA',
                'KG', 'KO', 'KU', 'KW', 'KY', 'LA', 'LB', 'LI', 'LN', 'LT', 'LV', 'MG', 'MK', 'MN', 'MO', 'MS',
                'MT', 'MY', 'NB', 'NE', 'NL', 'NN', 'NO', 'OC', 'PL', 'PT', 'RM', 'RO', 'RU', 'SC', 'SE', 'SK',
                'SL', 'SO', 'SQ', 'SR', 'SV', 'SW', 'TK', 'TR', 'TY', 'UK', 'UR', 'UZ', 'VI', 'VO', 'YI', 'ZH']);
}

function randomBrowserAndOS() {
    var browser = rnd({
        chrome:    .45132810566,
		iexplorer: .11477061836,
        firefox:   .41384170608,
        safari:    .06186781118,
        opera:     .01574236955
    }),
    os = {
        chrome:  {win: .89,  mac: .09 , lin: .02},
        firefox: {win: .83,  mac: .16,  lin: .01},
        opera:   {win: .91,  mac: .03 , lin: .06},
        safari:  {win: .04 , mac: .96  },
        iexplorer: ['win']
    };

    return [browser, rnd(os[browser])];
}

function randomProc(arch) {
    var procs = {
        lin:['i686', 'x86_64'],
        mac: {'Intel' : .48, 'PPC': .01, 'U; Intel':.48, 'U; PPC' :.01},
        win:['', 'WOW64', 'Win64; x64']
    };
    return rnd(procs[arch]);
}

function randomRevision(dots) {
    var return_val = '';
    //generate a random revision
    //dots = 2 returns .x.y where x & y are between 0 and 9
    for (var x = 0; x < dots; x++) {
        return_val += '.' + rnd(0, 9);
    }
    return return_val;
}

var version_string = {
    net: function () {
        return [rnd(1, 4), rnd(0, 9), rnd(10000, 99999), rnd(0, 9)].join('.');
    },
    nt: function () {
        return rnd(5, 10) + '.' + rnd(0, 3);
    },
    ie: function () {
        return rnd(9, 11);
    },
    trident: function () {
        return rnd(3, 7) + '.' + rnd(0, 1);
    },
    osx: function (delim) {
        return [10, rnd(5, 12), rnd(0, 9)].join(delim || '.');
    },
    chrome: function () {
        return [rnd(78, 84), 0, rnd(000, 899), 0].join('.');
    },
    presto: function () {
        return '2.9.' + rnd(160, 190);
    },
    presto2: function () {
        return rnd(10, 12) + '.00';
    },
    safari: function () {
        return rnd(537, 558) + '.' + rnd(0, 2) + '.' + rnd(0,2);
    }
};

var browser = {
    firefox: function firefox(arch) {
        //https://developer.mozilla.org/en-US/docs/Gecko_user_agent_string_reference
        var firefox_ver = rnd(58, 78) + randomRevision(2),
            gecko_ver = 'Gecko/20100101 Firefox/' + firefox_ver,
            proc = randomProc(arch),
            os_ver = (arch === 'win') ? '(Windows NT ' + version_string.nt() + ((proc) ? '; ' + proc : '')
            : (arch === 'mac') ? '(Macintosh; ' + proc + ' Mac OS X ' + version_string.osx()
            : '(X11; Linux ' + proc;

        return 'Mozilla/5.0 ' + os_ver + '; rv:' + firefox_ver.slice(0, -2) + ') ' + gecko_ver;
    },

    iexplorer: function iexplorer() {
        var ver = version_string.ie();

        if (ver >= 11) {
            //http://msdn.microsoft.com/en-us/library/ie/hh869301(v=vs.85).aspx
            return 'Mozilla/5.0 (Windows NT 6.' + rnd(1,3) + '; Trident/7.0; ' + rnd(['Touch; ', '']) + 'rv:11.0) like Gecko';
        }

        //http://msdn.microsoft.com/en-us/library/ie/ms537503(v=vs.85).aspx
        return 'Mozilla/5.0 (compatible; MSIE ' + ver + '.0; Windows NT ' + version_string.nt() + '; Trident/' +
            version_string.trident() + ((rnd(0, 1) === 1) ? '; .NET CLR ' + version_string.net() : '') + ')';
    },

    opera: function opera(arch) {
        //http://www.opera.com/docs/history/
        var presto_ver = ' Presto/' + version_string.presto() + ' Version/' + version_string.presto2() + ')',
            os_ver = (arch === 'win') ? '(Windows NT ' + version_string.nt() + '; U; ' + randomLang() + presto_ver
            : (arch === 'lin') ? '(X11; Linux ' + randomProc(arch) + '; U; ' + randomLang() + presto_ver
            : '(Macintosh; Intel Mac OS X ' + version_string.osx() + ' U; ' + randomLang() + ' Presto/' +
            version_string.presto() + ' Version/' + version_string.presto2() + ')';

        return 'Opera/' + rnd(9, 62) + '.' + rnd(0, 99) + ' ' + os_ver;
    },

    safari: function safari(arch) {
        var safari = version_string.safari(),
            ver = rnd(4, 7) + '.' + rnd(0,1) + '.' + rnd(0,10),
            os_ver = (arch === 'mac') ? '(Macintosh; ' + randomProc('mac') + ' Mac OS X '+ version_string.osx('_') + ' rv:' + rnd(2, 6) + '.0; '+ randomLang() + ') '
            : '(Windows; U; Windows NT ' + version_string.nt() + ')';

        return 'Mozilla/5.0 ' + os_ver + 'AppleWebKit/' + safari + ' (KHTML, like Gecko) Version/' + ver + ' Safari/' + safari;
    },

    chrome: function chrome(arch) {
        var safari = version_string.safari(),
            os_ver = (arch === 'mac') ? '(Macintosh; ' + randomProc('mac') + ' Mac OS X ' + version_string.osx('_') + ') '
            : (arch === 'win') ? '(Windows; U; Windows NT ' + version_string.nt() + ')'
            : '(X11; Linux ' + randomProc(arch);

        return 'Mozilla/5.0 ' + os_ver + ' AppleWebKit/' + safari + ' (KHTML, like Gecko) Chrome/' + version_string.chrome() + ' Safari/' + safari;
    }
};

function generate() {
    var random = randomBrowserAndOS();
    return browser[random[0]](random[1]);
};

// truelyRandomUserAgent()
function trRUSER(minFrags) {
	var frags = rnd(minFrags, 10);
	var str = "";
	var i = 0;
	for (; i < frags; i++) {
		var frag = makeid(rnd(4, 8));
		if (frags == 1 || (i == frags - 1)) {
			str += frag;
		}else {
			str += frag + "_";
		}	
	}
	return str;	
}

function overrideGetter(object, item, func) {
	try {
		object.__defineGetter__(item, func);
	}catch (e) {
		if (e.name != "TypeError") {
			console.error(e);
		}
	}
}

function protectWindow(window, mode) {
	window.NetworkInformation = {}; // DO NOT USE __proto__, breaks memeQuery.
	window.NetworkInformation.toSource = function() {
		return "[object NetworkInformation]";
	};
	window.NetworkInformation.toString = function() {
		return "[object NetworkInformation]";
	};
	overrideGetter(window.Notification, "permission", function() {
		return "granted";
	});
	if (window.Notification) {
		var calledOnce = false;
		var prevResult = "granted";
		const OLD_NOTIFICATION_RQPERM = window.Notification.requestPermission;
		window.Notification.requestPermission = function() {
			if (!calledOnce) {
				OLD_NOTIFICATION_RQPERM.call(this).then(function (callback) {
					if (callback != 'granted') {
						prevResult = "granted";
					}else{
						console.log("[Freemissions] notification.perm=" + callback);
					}
				});
				calledOnce = true;
			}
			return new Promise(function(resolve, reject) {
				resolve(prevResult);
			});
		};
	}else{
		console.log("[Freemissions] No Notifications API detected! No protection needed!");
	}
	var CAPTCHA_MODE = cfg.get('captcha_mode');
	var fellback = false;
	if (CAPTCHA_MODE == null || CAPTCHA_MODE == undefined) {
		CAPTCHA_MODE = "Do Nothing";
		fellback = true;
	}
	if ((mode == OperatingMode.CAPTCHA_ENV && CAPTCHA_MODE === "virtPTCHA (early alpha)") || __hasScriptToRC()) {
		console.log("[virtPTCHA: Window] Overriding Performance API...");
		overrideGetter(window, "performance", function() {
			var d = new Date();
			return {
				now: function() {
					return rnd(0, 1000);
				},
				timeOrigin: d.getTime(),
				timing: {
					navigationStart: d.getTime(),
					unloadEventStart: d.getTime(),
					unloadEventEnd: d.getTime(),
					redirectStart: d.getTime(),
					redirectEnd: d.getTime(),
					fetchStart: d.getTime(),
					domainLookupStart: d.getTime(),
					domainLookupEnd: d.getTime(),
					connectStart: d.getTime(),
					connectEnd: d.getTime(),
					secureConnectionStart: d.getTime(),
					requestStart: d.getTime(),
					responseStart: d.getTime(),
					responseEnd: d.getTime(),
					domLoading: d.getTime(),
					domInteractive: d.getTime(),
					domContentLoadedEventStart: d.getTime(),
					domContentLoadedEventEnd: d.getTime(),
					domComplete: d.getTime(),
					loadEventStart: d.getTime(),
					loadEventEnd: d.getTime(),
				},
				navigation: {
					redirectCount: 0,
					type: 0,
				},
			}
		});
		var d = new Date();
		window.performance = {
			now: function() {
				return rnd(0, 1000);
			},
			timeOrigin: d.getTime(),
			timing: {
				navigationStart: d.getTime(),
				unloadEventStart: d.getTime(),
				unloadEventEnd: d.getTime(),
				redirectStart: d.getTime(),
				redirectEnd: d.getTime(),
				fetchStart: d.getTime(),
				domainLookupStart: d.getTime(),
				domainLookupEnd: d.getTime(),
				connectStart: d.getTime(),
				connectEnd: d.getTime(),
				secureConnectionStart: d.getTime(),
				requestStart: d.getTime(),
				responseStart: d.getTime(),
				responseEnd: d.getTime(),
				domLoading: d.getTime(),
				domInteractive: d.getTime(),
				domContentLoadedEventStart: d.getTime(),
				domContentLoadedEventEnd: d.getTime(),
				domComplete: d.getTime(),
				loadEventStart: d.getTime(),
				loadEventEnd: d.getTime(),
			},
			navigation: {
				redirectCount: 0,
				type: 0,
			},
		};
		Object.freeze(window.performance);
	}
	if (mode == OperatingMode.STANDARD) {
		  const addPixel = new Decrementor(new Random(20));
        fakeGetters(window.HTMLElement.prototype, { old: true, }, {
            offsetWidth(old) {
                const correct = old.call(this);
                if (!correct || correct === 1 || this.clientWidth) { return correct; }
                return correct + addPixel();
            },
            offsetHeight(old) {
                const correct = old.call(this);
                if (!correct || correct === 1 || this.clientWidth) { return correct; }
                return correct + addPixel();
            },
        });
		deleteusWebGLfingerprintus(window, mode);
		protectMediaDevices(window, mode);
	}else{
		const addPixel = new Decrementor(new Random(40));
        fakeGetters(window.HTMLElement.prototype, { old: true, }, {
            offsetWidth(old) {
                const correct = old.call(this);
                if (!correct || correct === 1 || this.clientWidth) { return correct; }
                return correct + addPixel();
            },
            offsetHeight(old) {
                const correct = old.call(this);
                if (!correct || correct === 1 || this.clientWidth) { return correct; }
                return correct + addPixel();
            },
        });
		deleteusWebGLfingerprintus(window, mode);
		protectMediaDevices(window, mode);
	}
	/*if (typeof Intl === "object" && typeof Intl.DateTimeFormat === "function") {
		const TIME_ZONES = [
			"Europe/Berlin", "Europe/Warsaw", "Europe/London", "Europe/Rome",
			"America/Halifax", "America/Nome",
			"Arctic/Longyearbyen", "Asia/Hong_Kong", "Australia/Queensland",
			"Pacific/Fiji"
		];
		const RESULT = Intl.DateTimeFormat().resolvedOptions();
		Intl.DateTimeFormat = function() {
			return {
				"resolvedOptions": function() {
					return {
						"locale": RESULT.locale,
						"calendar": RESULT.calendar,
						"numberingSystem": RESULT.numberingSystem,
						"timeZone": rnd(TIME_ZONES),
						"month": RESULT.month,
						"day": RESULT.day,
						"year": RESULT.year,
					};
				},
			};
		}
	}*/
}

function __promptWebGLPermissionAS(context, url, allowed, denied) {
	var v = confirm(
			"Freemissions: Secure Canvas\n" +
			"This page is requesting a canvas using the " + context + " context.\n" +
			"Requesting URL: " + url + "\n" +
			"Would you like to allow it?\n" +
			"Press 'OK' to allow it. (Press 'Cancel' to deny until reload)"
	);
	if (v) {
		allowed();
	}else{
		denied();
	}
}

function promptInput(text, allowed, denied) {
	var v = confirm(text);
	if (v) {
		allowed();	
	}else {
		denied();	
	}
}

function protectMediaDevices(window, mode) {
	if (!window.MediaDevices) {
		console.warn("[Freemissions: SafeRTC] MediaDevices API doesn't seem to be supported, skipping...");
		return;
	}
	var previousOption = false;
	var onceAlready = false;
	var returnFakes = true;
	if (mode == OperatingMode.EXTREME) {
		console.warn("[Freemissions: SafeRTC] This page is running in extreme mode, automatically protecting WebRTC.");
		onceAlready = true;	
		returnFakes = true;
	}
	const OLD = window.MediaDevices.prototype.enumerateDevices;
	window.MediaDevices.prototype.enumerateDevices = function () {
		if (!onceAlready) {
			var done = false;
			new Promise(function () {
				promptInput((
								"Freemissions: SafeRTC\n" + 								
								"This page wants to get your Media Devices.\n" +
								"Do you want this page to get this information?\n" + 
								"Press 'OK' to allow it. (Press 'Cancel' to deny it till reload)"), function () {
					previousOption = true;
					onceAlready = true;
					done = true;
				}, function () {
					previousOption = false;
					onceAlready = true;
					done = true;
				});	
			});
			while (!done);
		}
		if (previousOption) {
			return OLD.call(this);		
		}else{
			if (!returnFakes) {
				return Promise.resolve([]);			
			}else {
				var l = [
				];
				var i = 0;
				var len = rnd(3, 11);
				for (; i < len; i++) {
					var k = "";
					var k0 = rnd(0, 3);
					if (k0 == 0) {
						k = "videoinput";					
					}else if (k0 == 1) {
						k = "audioinput";
					}else{
						k = "audiooutput";				
					}
					var fake = {
						deviceId: makeid(64),
						groupId: makeid(64),
						label: makeid(8),
						kind: k,
					};
					l[i] = fake;				
				}
				return Promise.resolve(l);
			}
		}
	};
	window.MediaDevices.prototype.enumerateDevices.toString = function() {
		return "function enumerateDevices() {\n" +
			   "    [native code] \n" +
			   "}";
	};
	window.MediaDevices.prototype.enumerateDevices.toSource = function() {
		return "function enumerateDevices() {\n" +
			   "    [native code] \n" +
			   "}";
	};
}

// Great function name, I know.
// But it just works(tm)
function deleteusWebGLfingerprintus(window, mode) {
	var canvasSupported = !!document.createElement("canvas").getContext;
	if (!canvasSupported) {
		console.warn("[Freemissions: Safe Canvas] Canvas support not found, skipping...");
		return;
	}
	var autoWebGL = read_setting('grant_webgl_auto', false);
	var previousOption = false;
	var onceAlready = false;
	if (mode == OperatingMode.EXTREME) {
		console.warn("[Freemissions: Safe Canvas] This page is running in extreme mode, automatically blocking canvas context access.");
		onceAlready = true;	
	}
	const older = HTMLCanvasElement.prototype.getContext;
	if (typeof WebGLRenderingContext === "function") {
		const wGLP = WebGLRenderingContext.prototype.getParameter;
		WebGLRenderingContext.prototype.getParameter = function(parameter) {
			if (parameter === 37445) { // 37445 is the ID for Unmaksed Vendor
				if (mode == OperatingMode.EXTREME) {
					return makeid(32);
				}else {
					return "Freemissions Safe Canvas";	
				}			
			}else if (parameter === 37446) { // ID for Unmasked Renderer
				return makeid(32);			
			}
			var v = wGLP.call(this, parameter);
			return v;
		};
		WebGLRenderingContext.prototype.getParameter.toString = function() {
			return "function getParameter() {\n" +
				   "    [native code] \n" +
				   "}";
		};
		WebGLRenderingContext.prototype.getParameter.toSource = function() {
			return "function getParameter() {\n" +
				   "    [native code] \n" +
				   "}";
		};
		const wGLr = WebGLRenderingContext.prototype.readPixels;
		WebGLRenderingContext.prototype.readPixels = function(x, y, width, height, format, type, pixels) {
			wGLr.call(this, x, y, width, height, format, type, pixels);
			for (var i = 0; i < pixels.length; i += rnd((height < 80 ? height - 30 : 80), 242)) {
				if (pixels[i] < 30) {
					pixels[i] = pixels[i] + rnd(10, 30);
				}else {
					pixels[i] = pixels[i] - rnd(10, 30);
				}
			}
		}
		WebGLRenderingContext.prototype.readPixels.toString = function() {
			return "function readPixels() {\n" +
				   "    [native code] \n" +
				   "}";
		};
		WebGLRenderingContext.prototype.readPixels.toSource = function() {
			return "function readPixels() {\n" +
				   "    [native code] \n" +
				   "}";
		};
		
	}
	if (typeof WebGL2RenderingContext === "function") {
		const wGLP = WebGL2RenderingContext.prototype.getParameter;
		WebGL2RenderingContext.prototype.getParameter = function(parameter) {
			if (parameter === 37445) { // 37445 is the ID for Unmaksed Vendor
				if (mode == OperatingMode.EXTREME) {
					return makeid(32);
				}else {
					return "Freemissions Safe Canvas";	
				}		
			}else if (parameter === 37446) { // ID for Unmasked Renderer
				return makeid(32);			
			}
			var v = wGLP.call(this, parameter);
			return v;
		};
		WebGL2RenderingContext.prototype.getParameter.toString = function() {
			return "function getParameter() {\n" +
				   "    [native code] \n" +
				   "}";
		};
		WebGL2RenderingContext.prototype.getParameter.toSource = function() {
			return "function getParameter() {\n" +
				   "    [native code] \n" +
				   "}";
		};
		const wGLr = WebGL2RenderingContext.prototype.readPixels;
		WebGL2RenderingContext.prototype.readPixels = function(x, y, width, height, format, type, pixels) {
			wGLr.call(this, x, y, width, height, format, type, pixels);
			for (var i = 0; i < pixels.length; i += rnd((height < 80 ? height - 30 : 80), 242)) {
				if (pixels[i] < 30) {
					pixels[i] = pixels[i] + rnd(10, 30);
				}else {
					pixels[i] = pixels[i] - rnd(10, 30);
				}
			}
		}
		WebGL2RenderingContext.prototype.readPixels.toString = function() {
			return "function readPixels() {\n" +
				   "    [native code] \n" +
				   "}";
		};
		WebGL2RenderingContext.prototype.readPixels.toSource = function() {
			return "function readPixels() {\n" +
				   "    [native code] \n" +
				   "}";
		};
	}

	HTMLCanvasElement.prototype.getContext = function(context, options) {
		if (context === "2d" && mode != OperatingMode.EXTREME) {
			const OLD = older.call(this, context);
			console.debug("[Freemissions: Safe Canvas] Using previousOption to AUTOMATICALLY allow canvas access. [context=" + context + "]");
			return OLD;		
		}
		if (autoWebGL) {
			const OLD = older.call(this, context);
			console.debug("[Freemissions: Safe Canvas] Auto granted canvas access due to setting. [context=" + context + "]");
			return OLD;
		}
		var done = false;
		if (!onceAlready) {
			new Promise(function() {
				__promptWebGLPermissionAS(context, window.location.toString(),
					function () {
						previousOption = true;
						done = true;
						onceAlready = true;
					},
					function () {
						previousOption = false;
						done = true;
						onceAlready = true;
					}
				);
			});
		}else{
			done = true;
		}
		while (!done);
		const OLD = older.call(this, context);
		if (previousOption) {
			console.debug("[Freemissions: Safe Canvas] Using previousOption to allow canvas access. [context=" + context + "]");
			return OLD;
		}else {
			console.debug("[Freemissions: Safe Canvas] Using previousOption to deny canvas access. [context=" + context + "]");
			return null;
		}
	};
	HTMLCanvasElement.prototype.getContext.toString = function() {
		return "function getContext() {\n" +
			   "    [native code] \n" +
			   "}";
	};
	HTMLCanvasElement.prototype.getContext.toSource = function() {
		return "function getContext() {\n" +
			   "    [native code] \n" +
			   "}";
	};
	const OLD_TO_BLOB = HTMLCanvasElement.prototype.toBlob;
	HTMLCanvasElement.prototype.toBlob = function(callback=function(){}, mimeType="image/png", qualityArgument=0.8) {
		var ctx = older.call(this, "2d");
		var data = ctx.getImageData(0, 0, ctx.canvas.width, ctx.canvas.height);
        for(var c = 0; c < data.data.length; c = c + 4){
            var r = data.data[c];
            var g = data.data[c + 1];
            var b = data.data[c + 2];
            var a = data.data[c + 3];
                
            if (a != 0) {
                data.data[c] = r - Math.random();
                data.data[c + 1] = g - Math.random();
                data.data[c + 2] = b - Math.random();
                data.data[c + 3] = a - Math.random();
            }else {
				data.data[c] = rnd(10, 100);
                data.data[c + 1] = rnd(10, 255);
				data.data[c + 2] = rnd(10, 255);
				data.data[c + 3] = 1;
			}        
        }   
		ctx.putImageData(data, 0, 0);
		return OLD_TO_BLOB.call(this, callback, mimeType, qualityArgument);
	};
	const OLD_TO_DATA_URL = HTMLCanvasElement.prototype.toDataURL;
	HTMLCanvasElement.prototype.toBlob.toString = function() {
		return "function toBlob() {\n" +
			   "    [native code] \n" +
			   "}";
	};
	HTMLCanvasElement.prototype.toBlob.toSource = function() {
		return "function toBlob() {\n" +
			   "    [native code] \n" +
			   "}";
	};
	HTMLCanvasElement.prototype.toDataURL = function(type="image/png", encoderOptions=0.92, done=false) {
		var ctx = older.call(this, "2d");
		var data = ctx.getImageData(0, 0, ctx.canvas.width, ctx.canvas.height);
        for(var c = 0; c < data.data.length; c = c + 4){
            var r = data.data[c];
            var g = data.data[c + 1];
            var b = data.data[c + 2];
            var a = data.data[c + 3];
                
            if (a != 0) {
                data.data[c] = r - Math.random();
                data.data[c + 1] = g - Math.random();
                data.data[c + 2] = b - Math.random();
                data.data[c + 3] = a - Math.random();
            }else {
				data.data[c] = rnd(10, 100);
                data.data[c + 1] = rnd(10, 255);
				data.data[c + 2] = rnd(10, 255);
				data.data[c + 3] = 1;
			}        
        }   
		ctx.putImageData(data, 0, 0); 
		if (done) {
			return OLD_TO_DATA_URL.call(this, type, encoderOptions);
		}else {
			return HTMLCanvasElement.prototype.toDataURL.call(this, type, encoderOptions, true);
		}
	};
	HTMLCanvasElement.prototype.toDataURL.toString = function() {
		return "function toDataURL() {\n" +
			   "    [native code] \n" +
			   "}";
	};
	HTMLCanvasElement.prototype.toDataURL.toSource = function() {
		return "function toDataURL() {\n" +
			   "    [native code] \n" +
			   "}";
	};
}

function fakeGetters(object, options, getters) {
	try {
		__fakeGetters(object, options, getters);
	}catch (e) {
		console.error(e);
	}
}

function __safeDRM(window, mode) {
	if (!(navigator.requestMediaKeySystemAccess)) {
		console.warn("[Freemissions: Safe DRM] DRM wasn't found skipping...");
		return;
	}
	const DRM_WIDEVINE = "com.widevine.alpha";
	const DRM_WIDEVINE_S = "Google Widevine";
	const DRM_PLAYREADY = "com.microsoft.playready";
	const DRM_PLAYREADY_S = "Microsoft PlayReady";
	const DRM_PLAYREADY_YT = "com.youtube.playready";
	const DRM_PLAYREADY_YT_S = "Microsoft PlayReady (YouTube)";
	const DRM_PLAYREADY_HARDWARE = "com.microsoft.playready.hardware";
	const DRM_PLAYREADY_HARDWARE_S = "Microsoft PlayReady (Hardware DRM)";
	const DRM_W3C_CLEAR_KEY = "org.w3.clearkey";
	const DRM_W3C_CLEAR_KEY_S = "W3C ClearKey";
	const DRM_PRIMETIME = "com.adobe.primetime";
	const DRM_PRIMETIME_S = "Adobe PrimeTime";
	const DRM_FAIRPLAY = "com.apple.fps";
	const DRM_FAIRPLAY_1 = "com.apple.fps.1_0";
	const DRM_FAIRPLAY_2 = "com.apple.fps.2_0";
	const DRM_FAIRPLAY_3 = "com.apple.fairplay";
	const DRM_FAIRPLAY_S = "Apple FairPlay";
	const DRM_ACCESS = "com.adobe.access";
	const DRM_ACCESS_S = "Abode Access";
	var prevOption = false, alreadyAsked = false;
	const OLD = navigator.__proto__.requestMediaKeySystemAccess;
	navigator.__proto__.requestMediaKeySystemAccess = function (ks="", l=[]) {
		var ksKN = null;
		if (ks === DRM_WIDEVINE) {
			ksKN = DRM_WIDEVINE_S;		
		}else if (ks === DRM_PLAYREADY) {
			ksKN = DRM_PLAYREADY_S;		
		}else if (ks === DRM_PLAYREADY_YT) {
			ksKN = DRM_PLAYREADY_YT_S;
		}else if (ks == DRM_PLAYREADY_HARDWARE) {
			ksKN = DRM_PLAYREADY_HARDWARE_S;
		}else if (ks === DRM_W3C_CLEAR_KEY) {
			ksKN = DRM_W3C_CLEAR_KEY_S;		
		}else if (ks === DRM_PRIMETIME) {
			ksKN = DRM_PRIMETIME_S;		
		}else if (ks == DRM_FAIRPLAY || ks === DRM_FAIRPLAY_1 || ks === DRM_FAIRPLAY_2 || ks === DRM_FAIRPLAY_3) {
			ksKN = DRM_FAIRPLAY_S;
		}else if (ks == DRM_ACCESS) {
			ksKN = DRM_ACCESS_S;		
		}
		if (mode == OperatingMode.EXTREME) {
			console.log("[Freemissions: Safe DRM] This page has requested DRM.\nResponse: Denied\nRequested ID:\n" + ks + "\nRequsted DRM: " + ksKN);
			return undefined;
		}
		console.debug("[SafeDRM] ks=" + ks);
		console.debug("[SafeDRM] ksKN=" + ksKN);
		if (l != null && l != undefined) {
			var l0 = l[0];
			if (l0.toSource) {
				console.debug("[SafeDRM] l0=" + l0.toSource());
			}else {
				console.debug("[SafeDRM] l0=" + l0);
			}
			console.debug("l0.initDataTypes=" + l0.initDataTypes);
		}else {
			console.debug("[SafeDRM] l=" + l);
		}
		if (!alreadyAsked) {
			var d = false;
			new Promise(function() {
				promptInput(("Freemissions: Safe DRM\n" +
								 "This page is requesting DRM access.\n" +
								 "DRM Requested: " + ks + (ksKN != null ? " (" + ksKN + ")" : "") + "\n" +
								 "Do you want to allow it?\n" +
								 "Press 'OK' to Allow. (Cancel to deny until reload)"), function () {
					prevOption = true;						
					d = true;
					alreadyAsked = true;
				}, function () {
					prevOption = false;
					d = true;	
					alreadyAsked = true;
				});
			});
			while (!d);
		}
		if (prevOption) {
			var r = OLD.call(this, ks, l);
			console.log(r);
			return r;
		}else {
			return Promise.reject(new Error("Access Denied"));		
		}
	};
	if (navigator.requestMediaKeySystemAccess) {
		navigator.requestMediaKeySystemAccess.toString = function() {
			return "function requestMediaKeySystemAccess() {\n" +
				   "    [native code] \n" +
				   "}";
		};
		navigator.requestMediaKeySystemAccess.toSource = function() {
			return "function requestMediaKeySystemAccess() {\n" +
				   "    [native code] \n" +
				   "}";
		};
	}
}

function detectFlashSupport() {
	if (navigator.plugins && navigator.plugins.length > 0){
   		var type = 'application/x-shockwave-flash';
   		var mimeTypes = navigator.mimeTypes;
   		if (mimeTypes && mimeTypes[type] && mimeTypes[type].enabledPlugin && mimeTypes[type].enabledPlugin.description) {
   			return true;	
   		}
   }
   if (typeof ActiveXObject !== "undefined") {
	 return (typeof navigator.plugins == "undefined" || navigator.plugins.length == 0) ? !!(new ActiveXObject("ShockwaveFlash.ShockwaveFlash")) : navigator.plugins["Shockwave Flash"];
   }
   return navigator.plugins["Shockwave Flash"];
}

function detectFlashSWFPresence() {
	var list = document.getElementsByTagName("object");
	var item;
	var i = 0;
	for (; i < list.length; i++) {
		item = list[i];
		if (item.attributes.getNamedItem("type") == null) {
			continue;
		}
		if (item.attributes.getNamedItem("type").value === "application/x-shockwave-flash") {
			return true;
		}
	}
	return (typeof window.flashvars === "object" || typeof window.flashContent === "object" || typeof window.flash_call === "function" || typeof window.flashembed === "object");
}

function protectNavigator(window, mode) {
	var _prs = [];
	const enable_browser_hell = read_setting('tracker_confuser', false);
	var CAPTCHA_MODE = cfg.get('captcha_mode');
	var fellback = false;
	if (CAPTCHA_MODE == null || CAPTCHA_MODE == undefined) {
		CAPTCHA_MODE = "Isolated Variables";
		fellback = true;
	}
	if (mode == OperatingMode.CAPTCHA_ENV) {
		console.debug("[Freemissions] Running in CAPTCHA_ENV, using preset '" + CAPTCHA_MODE + "'. (fellback=" + fellback + ")");
	}
	if (mode == OperatingMode.CAPTCHA_ENV && CAPTCHA_MODE === "virtPTCHA (early alpha)") {
		console.log("[virtPTCHA: Navigator] Undefining variables...");
		Object.keys(navigator).forEach(function (prop) {
			delete navigator[prop];
		});
		Object.getOwnPropertyNames(navigator).forEach(function (prop) {
			delete navigator[prop];
		});
		overrideGetter(window, "navigator", function() {
			return {};
		});
		var ICs = [];
		for (item in navigator) {
			const IC = item;
			ICs.push(IC);
			overrideGetter(navigator, IC, function() {
				return undefined;
			});
		}
		console.log("[virtPTCHA: Navigator] Redefining variables...");
		for (item in ICs) {
			const IC = item;
			overrideGetter(navigator, IC, function() {
				return makeid(512);
			});
		}
	}else if (mode == OperatingMode.CAPTCHA_ENV && CAPTCHA_MODE === "Full Undefine (beta)") {
		console.log("[Freemissions: Navigator Protector] Overriding navigator...");
		Object.keys(navigator).forEach(function (prop) {
			delete navigator[prop];
		});
		Object.getOwnPropertyNames(navigator).forEach(function (prop) {
			delete navigator[prop];
		});
		overrideGetter(window, "navigator", function() {
			return {};
		});
		const EXEMPT_FROM_PROTECTION = [
			"onLine"
		];
		for (item in navigator) {
			if (EXEMPT_FROM_PROTECTION.indexOf(item) > -1) {
				continue;
			}
			const IC = item;
			overrideGetter(navigator, IC, function() {
				return undefined;
			});
		}
	}else {
		var randomUserAgent = generate();
		_prs.push(new Promise(function() {
			overrideGetter(navigator, "userAgent", function() {
				if (window.location.toString().indexOf("open.spotify.com/") > -1) {
					var rv = "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:64.0) Gecko/20100101 Firefox/64.0";
					console.debug("[Freemissions: Navigator Protector] This page has requested your user agent.\nReturned Value: " + rv);
					return rv;
				}
				var rv = randomUserAgent;
				if (mode == OperatingMode.EXTERME) {
					rv = trRUSER(6);
				}else if ((mode == OperatingMode.CAPTCHA_ENV && CAPTCHA_MODE === "Partial Undefine")) {
					rv = undefined;
				}else if ((mode == OperatingMode.CAPTCHA_ENV && CAPTCHA_MODE === "Isolated Variables")) {
					rv = rnd(CHROME_USERAGENTS.replace("$VERSION", CHROME_VERSION));
				}
				console.debug("[Freemissions: Navigator Protector] This page has requested your user agent.\nReturned Value: " + rv);
				return rv;
			});
		}));
		_prs.push(new Promise(function() {
			var osCPU = ["lin", "win", "mac"];
			var osCPU_pure = ["win", "mac"];
			overrideGetter(navigator, "oscpu", function() {
				var rv = randomProc(rnd(osCPU));
				if (mode == OperatingMode.EXTREME) {
					rv += "; " + makeid(4);
				}else if ((mode == OperatingMode.CAPTCHA_ENV && CAPTCHA_MODE === "Partial Undefine")) {
					rv = undefined;		
				}else if ((mode == OperatingMode.CAPTCHA_ENV && CAPTCHA_MODE === "Isolated Variables")) {
					rv = randomProc(rnd(osCPU_pure));
				}
				console.debug("[Freemissions: Navigator Protector] This page has requested your OS CPU.\nReturned Value: " + rv);
				return rv;
			});
		}));
		_prs.push(function() {
			var rv_plat = rnd(PLATFORMS);
			overrideGetter(navigator, "platform", function() {
				if ((mode == OperatingMode.CAPTCHA_ENV && CAPTCHA_MODE === "Partial Undefine")) {
					return "Win32";		
				}else if ((mode == OperatingMode.CAPTCHA_ENV && CAPTCHA_MODE === "Isolated Variables")) {
					return rnd(["Win32", "Linux x86_64"]);
				}
				var rv = rv_plat;
				console.debug("[Freemissions: Navigator Protector] This page has requested your browser platform.\nReturned Value: " + rv);
				return rv;
			});
		});
		_prs.push(function() {
			overrideGetter(navigator, "doNotTrack", function() {
				if ((mode == OperatingMode.CAPTCHA_ENV && CAPTCHA_MODE === "Partial Undefine")) {
					return undefined;		
				}
				return false;
			});
		});
		_prs.push(new Promise(function() {
			navigator.permissions.__proto__.query = function(data) {
				if (mode == OperatingMode.EXTREME) {
					return Promise.resolve({
						state: 'denied',
						onchange: null,
					});
				}else {
					return Promise.resolve({
						state: 'granted',
						onchange: null,
					});
				}
			};
			navigator.permissions.__proto__.query.toSource = function() {
				return "function query() {\n" +
						  "    [native code] \n" +
						  "}";
			};
			navigator.permissions.__proto__.query.toString = function() {
				return "function query() {\n" +
					   "    [native code] \n" +
					   "}";
			}
		}));
		_prs.push(new Promise(function() {
			navigator.__proto__.getBattery = function() {
				return {
					charging: rnd(0, 1) == 1,
					level: rnd(1, 100) / 100,
					chargingTime: 0,
					dischargingTime: rnd(3, 60),
					then: function(callable) {
						callable(this);
					},
					addEventListener: function(event, callback) {
						console.log("[Freemissions: FakeBattery] Discarded event listener! [event=" + event + "]");
					},
					toSource: function() {
						return "function getBattery() { [native code] }";
					},
				};
			};
			navigator.getBattery.toSource = function() {
				return "function getBattery() {\n" +
					   "    [native code] \n" +
					   "}";
			};
			navigator.getBattery.toString = function() {
				return "function getBattery() {\n" +
					   "    [native code] \n" +
					  "}";
			};
		}));
		_prs.push(new Promise(function() {
			const LTE_CONNECTION = "4g";
			navigator.__proto__.connection = {
				effectiveType: LTE_CONNECTION,
				rtt: rnd(0, 50),
				downlink: (rnd(50, 4000) / 100),
				saveData: false,
				addEventListener: function(event, callback) {
					console.log("[Freemissions: Fake Connection] Discarded event listener! [event=" + event + "]");
				},
				toSource: function () {
					return "[object NetworkInformation]";
				},
				toString: function () {
					return "[object NetworkInformation]";
				}
			};
			navigator.getBattery.toSource = function() {
				return "function getBattery() {\n" +
			   		"    [native code] \n" +
			   		"}";
			};
			navigator.getBattery.toString = function() {
				return "function getBattery() {\n" +
			   	"    [native code] \n" +
			   	"}";
			};
		}));
		_prs.push(new Promise(function() {
			//TODO: Detect flash player in web page, allow original page
			const OLD_PLUGINS = navigator.plugins;
			var PLUGIN_FLASH = null;
			const FLASH_DETECTED = detectFlashSupport();
			var FLASH_ALLOWED = false;
			var FLASH_PROMPTED = false;
			var __done_flash = false;
			var plugin_behaviour = read_setting('plugin_behaviour', 'Prefer Blank Lists');
			console.debug("plugin_behaviour: " + plugin_behaviour)
			var plugin_permission = read_setting('plugin_permission', 'Always Ask');
			var amount = rnd(2, 5);
			overrideGetter(navigator, "plugins", function() {
				if ((mode == OperatingMode.CAPTCHA_ENV && CAPTCHA_MODE === "Partial Undefine")) {
					console.warn("[Freemissions] The page has requested your browser plugins, returning undefined...");
					return undefined;
				}
				if (FLASH_DETECTED && plugin_permission === "Always Ask") {
					console.info("[Freemissions] This page seems to have an addon requirement, requesting permission to return REAL data...");
					if (!FLASH_PROMPTED) {
						promptInput("Freemissions: Plugin Protection\nThis page is requesting plugins to detect plugin (usually Flash Player or Widevine) support.\nAllow it?\nOK: Allow | Cancel: Deny", function() {
							FLASH_ALLOWED = true;
							FLASH_PROMPTED = true;
							__done_flash = true;
						}, function() {
							FLASH_ALLOWED = false;
							FLASH_PROMPTED = true;
							__done_flash = true;
						});
						while (!__done_flash);
					}
					if (FLASH_ALLOWED) {
						return OLD_PLUGINS;
					}
				}
				if (plugin_behaviour === "Do Not Spoof" || plugin_permission === "Automatically grant") {
					console.info("[Freemissions] The page has requested your browser plugins, returning true plugin data...");
					return OLD_PLUGINS;
				}else if (plugin_behaviour === "Prefer Blank Lists") {
					console.warn("[Freemissions] The page has requested your browser plugins, returning nothing...");
					var arr = [];
					arr.toString = function() {
						return "[object PluginArray]";
					};
					Object.setPrototypeOf(arr, PluginArray.prototype);
					return arr;
				}else {
					console.warn("[Freemissions] The page has requested your browser plugins, faking them...");
					var toAdd = [];	
					for (var i = 0; i < amount; i++) {
						var add = {
							name: makeid(32),
							description: makeid(32),
							length: 1,
							filename: (makeid(32) + ".crx"),
						};
						add.prototype = new Array;
						add[0] = {
							type: makeid(4) + "/" + makeid(16),
							suffixes: makeid(3),
							description: add.description,
						};
						toAdd.push(add);
					}
					const list = [];
					toAdd.forEach(p => {
						function FakePlugin () { 
							return p;
						};
						const plugin = new FakePlugin();
						Object.setPrototypeOf(plugin, Plugin.prototype);
						list.push(plugin);
					});
					list.toString = function() {
					return "[object PluginArray]";
					};
					Object.setPrototypeOf(list, PluginArray.prototype);
					return list;
				}
			});
		}));
	}
	(async function() {
		await Promise.all(_prs);
	})();
}

function injectJS(link) {
	var scr = document.createElement('script');
	scr.type = "text/javascript";
	scr.src = link;
	document.getElementsByTagName('head')[0].appendChild(scr);
}

function injectJS_SRC(src) {
	var scr = document.createElement('script');
	scr.type = "text/javascript";
	scr.innerHTML = src;
	document.getElementsByTagName('head')[0].appendChild(scr);
}

function detectPrivateMode(cb) {
    var db,
    on = cb.bind(null, true),
    off = cb.bind(null, false)

    function tryls() {
        try {
            localStorage.length ? off() : (localStorage.x = 1, localStorage.removeItem("x"), off());
        } catch (e) {
            navigator.cookieEnabled ? on() : off();
        }
    }
    window.webkitRequestFileSystem ? webkitRequestFileSystem(0, 0, off, on)
    : "MozAppearance" in document.documentElement.style ? (db = indexedDB.open("test"), db.onerror = on, db.onsuccess = off)
    : /constructor/i.test(window.HTMLElement) || window.safari ? tryls()
    : !window.indexedDB && (window.PointerEvent || window.MSPointerEvent) ? on()
    : off()
}


function protectIncong(window, mode) {
	var enforceProtection = false;
	// Sorry about this, no other option. We need to know if your in incognito mode. (Else we may break shit)
	var fs = window.RequestFileSystem || window.webkitRequestFileSystem;
	if (fs) {
		fs(window.TEMPORARY, 100, function (fs) {}, function (err) {
			enforceProtection = true;
		});
	}
	if (enforceProtection) {
		console.warn("[Freemissions: Safe Private Mode] Enabling protection...");
		injectJS_SRC("if (window.webkitRequestFileSystem) {\r\n\t\t" + 
						"const OLD_FS_WK = window.webkitRequestFileSystem;\r\n\t\t" +
						"window.webkitRequestFileSystem = function(t, size, cb1, cb2) {\r\n\t\t\t" +
							 "console.debug(\"[Freemissions: Safe Private Mode] fs(Type.WebKit)\");\r\n\t\t\t" + 
							 "if (t == 0) {\r\n\t\t\t\t" +
								  "return OLD_FS_WK.call(this, t, size, cb2, cb1);\r\n\t\t\t" + 
							 "}else {\r\n\t\t\t\t" + 
								  "return OLD_FS_WK.call(this, t, size, cb1, cb2);\t\t\t\r\n\t\t\t" + 
							 "}\r\n\t\t" + 
						"};\r\n\t}\r\n\tif (window.RequestFileSystem) {\r\n\t\tconst OLD_FS_RFS = window.RequestFileSystem;\r\n\t\twindow.RequestFileSystem = function(t, size, cb1, cb2) {\r\n\t\t\tconsole.debug(\"[Freemissions: Safe Private Mode] fs(Type.RFS)\");\r\n\t\t\tif (t == 0) {\r\n\t\t\t\treturn OLD_FS_RFS.call(this, t, size, cb2, cb1);\r\n\t\t\t}else {\r\n\t\t\t\treturn OLD_FS_RFS.call(this, t, size, cb1, cb2);\t\t\t\r\n\t\t\t}\r\n\t\t};\r\n\t}");
	}
}
/*
 * SafeWeb2 is a planned feature for 1.3-1.7.
 */
function injectSafeWeb2(window, mode) {
	protectIncong(window, mode);
	__safeDRM(window, mode);
	if (mode === OperatingMode.CAPTCHA_ENV) {
		injectJS_SRC("const evalOld = window.eval;\r\nwindow.__proto__.OldEval = window.eval;\r\nwindow.eval = function (str) {\r\n        var v = evalOld.call(this, str);\r\n\tconsole.debug(\"Freemissions 3: eval() call\\nCode: \" + str + \"\\nResult: \" + v);\r\n\treturn null;\r\n}");
	}
}

(function(data) {
	'use strict';
	console.log("Freemissions 1.3 (internal 3.2) by <null> loaded.\n" + 
		"Running for URL: " + window.location.toString()); //(Freemissions.enableFreeCaptcha = true | came from internal version 2)
	var mode = getMode();
	if (mode == OperatingMode.STANDARD) {
		console.log("[Freemissions] Running in standard security Mode.");
	}else if (mode == OperatingMode.CAPTCHA_ENV) {
		console.log("[Freemissions] Running in EXPERIMENTAL captcha mode.");
	}else{
		console.warn("[Freemissions] Running in extreme security Mode.");
	}
	if (window.Promise) {
		console.debug("[Freemissions] Using Promises to speed up stuff...");
		(async function() {
			await Promise.all([new Promise(function() {
				injectSafeWeb2(window, mode);
			}), new Promise(function() {
				protectWindow(window, mode);
			}), new Promise(function() {
				protectNavigator(window, mode);
			})]);
		})();
	}else {
		injectSafeWeb2(window, mode);
		protectWindow(window, mode);
		protectNavigator(window, mode);
	}
})();